今回はJavaでファイルを暗号化してみる。InputStream/OutputStreamを暗号化するため、応用すれば通信路やオブジェクトの暗号化も可能である。ただ、通信路の暗号化としてはsshやsslといったプロトロコルがあるので、専用のライブラリを利用するほうがよいと思われる。
■ 暗号の基礎知識
暗号化とは簡単に言うと『他人が読めない文に変換すること』であり、対して『暗号化された文を人が読める文に変換すること』を復号化と呼ぶ。このとき『他人が読めない文』を暗号文、『人が読める文』を平文・復号文と呼び、変換するルールのことをアルゴリズムと呼ぶ。
簡単な例で言ってみると、刑事ドラマで犯人のことを「ホシ」と呼んでいたりするが、これも一種の暗号化である。「犯人=ホシ」という言い換え(アルゴリズム)が広まっていない頃は、「ホシが逃げました」という言葉を一般人が聞いても、何の話をしているかわからなかったはずである。しかし、刑事の間ではこの言い換え(アルゴリズム)が分かるので「ホシが逃げました=犯人が逃げました」という意味に解読(復号化)できるのである。
世界には多くの暗号化アルゴリズムがあり、例えばローマ帝国の英雄ジュリアス=シーザー(カエサル)が用いた暗号であるシーザー暗号や、第二次世界大戦でドイツ軍が採用したエニグマ暗号など知られているが、機密性・完全性・可用性の観点から今日のコンピュータの世界では以下に紹介するアルゴリズムがよく利用される。
図:シーザー暗号で文字列「Password」を暗号化・復号化する例
アルゴリズム
暗号化アルゴリズムは暗号化・復号化に利用される変換ルールのことであり、アルゴリズムにより暗号強度(暗号の解読難易度)が決定される。また、多くのアルゴリズムでは鍵と呼ばれる値を用いており、一般的には鍵長(値を表すbit数)が長いほど暗号強度は高くなる傾向にある。
鍵を利用する暗号化アルゴリズムを大きく分類すると、暗号化・復号化で同じ鍵を利用する『共通鍵暗号』方式と、暗号化・復号化で異なる鍵を利用する『公開鍵暗号』方式に分けられる。
図:『共通鍵暗号』と『公開鍵暗号』の動作イメージ。
イメージしにくいと思われるため補足すると、『公開鍵暗号』方式では公開鍵と秘密鍵という1対の鍵を利用する。この鍵ペアは「公開鍵で暗号化すると秘密鍵で復号化できる」、また反対に「秘密鍵で暗号化すると公開鍵で復号化できる」という2つの特徴を持つ。そして、公開鍵はweb上などで公開しておき、秘密鍵は誰にも見せないようにする。こうしておくと、公開されている受信者の公開鍵を使って暗号化することで、受信者のみが復号化できる。この仕組みのことを『公開鍵暗号』という。
『共通鍵暗号』と『公開鍵暗号』の代表的なアルゴリズムは以下のようなものである。
分類 |
アルゴリズム名 |
内容 |
共通鍵暗号 |
DES |
ブロック暗号(Feistel構造)
脆弱性があるため、利用は推奨されない |
DESede
(=triple DES) |
ブロック暗号(Feistel構造)
DESを3重にかけるアルゴリズム。
DESからAESへの移行が完了する期間に暫定的に利用された |
AES |
ブロック暗号(SPN構造) |
RC2 |
ブロック暗号(Feistel構造) |
RC4 |
ストリーム暗号。SSL,WEPで利用 |
RC5 |
ブロック暗号(Feistel構造) |
公開鍵暗号 |
RSA |
素因数分解問題の難しさを利用した暗号 |
DSA |
離散対数問題の難しさを利用した暗号 |
ECC |
楕円曲線暗号の難しさを利用した暗号 |
利用するアルゴリズムを選ぶ際には、各アルゴリズムの特徴を考慮することが大切である。例えば、『共通鍵暗号』方式については高速な暗号化・復号化処理を可能とするが、鍵を共有する(受け渡しする)際にセキュリティが確保されていないと鍵が他者に知られてしまうという欠点がある。対して『公開鍵暗号』では事前の鍵共有が必要ないため鍵が他者に渡る心配はないが、暗号化・復号化処理に時間がかかる。このため、実際に利用する際には『公開鍵暗号』(ディフィー・ヘルマン鍵共有など)で共通鍵を受け渡しし、『共通鍵暗号』で暗号化・復号化するという手法がとられることが多い。
また、アルゴリズムの暗号強度も考慮すべき内容である。例えば、1999年にDESの脆弱性を証明するために行われたコンテスト「DES CHALLENGE Ⅲ(主催RSA Security社=現EMC)」では、DES暗号が22時間15分で解読されていてAESへの切り替えを推奨しているし(
*3)、2015年5月に情報処理推進機構が発表したガイドライン(
*4)では、2030年まではtriple DESや鍵長2048bitのRSAを利用してもよいが、それ以降ではtriple DESについては利用しないよう、RSAについても鍵長を4096bitに伸ばすように推奨している。
ただし、暗号強度については法律により輸出制限がかかるため、JavaにおいてもJREやJDKを普通にインストールしただけでは暗号強度の高いアルゴリズムは利用できない(
*2)。例えばAESについては鍵長128bitまでしか利用できなくなっている。この制限を解除するためには「Java Cryptography Extension」という追加コンポーネントをインストールする必要があるが、利用地域において法的に問題ないかは自らの責任で判断することとなっているので注意が必要となる。
暗号利用モード
ブロック暗号アルゴリズムでは、一回の暗号化で変換される平文の長さは決まっており、鍵長(長くても数百バイト)程度の長さしか暗号化できない。より長い平文を暗号化する場合については、ブロック暗号アルゴリズムで暗号化できる文字数で長い平文をブロックへと分割し、それぞれのブロックに対して暗号化することになる。このとき、各ブロックにおいて暗号アルゴリズムの変換鍵を決定する方法を暗号利用モードと呼ぶ。
例えば、文字列「あんごうは、トラトラトラ」をブロック暗号「前後2文字を入れ替える」で暗号化する場合、ブロック暗号では2文字の文字列を対象としているため文字列を暗号化できない。しかし、文字列を2文字づつに区切ってブロックとすれば、各ブロック毎で暗号化することにより、文字列全体を暗号化できることになる。
注意点としては、各ブロックで同一の暗号化鍵を利用する場合(ECBモード)では、平文の文字パターンが暗号文にも表れてしまうことになる。これは例えば平文に『パスワード』という単語を多く含む文があり、その暗号文において『facgrd』という文字が多く現れる場合、暗号化アルゴリズムが分からなくても『facgrd』=『パスワード』という関連性が分かってしまうようなものである。このようなヒントが暗号文に現れないようにするため、前ブロックの情報を次ブロックの暗号化・復号化に利用するといった処理(前ブロックの暗号文とのXORをとってから暗号化するCBCなど)により暗号強度を高めることができる。この場合、最初のブロックには前ブロックの情報がないので、初期化ベクトルという値を指定して暗号化・復号化することになる。
よく利用される暗号利用モードの一覧を以下示す。(参考資料:
*5,
*6,
*7)
モード名 |
内容 |
ECB
(Electronic Codebook) |
各ブロックで同一の暗号化を行う。脆弱性があるため、
他のモードが使えない場合以外は利用が推奨されない |
CBC
(Cipher Block Chaining) |
前ブロックの暗号文と平文のXORをとってから暗号化を行う。
一般的に利用される。
暗号化の並列処理:不可
復号化の並列処理:可
ランダムリード :可 |
CFB
(Cipher Feedback) |
前ブロックの暗号文を暗号化し、平文とのXORをとる。
CBCにはない自己同期回復(ブロックの欠損・挿入からの回復)がある
暗号化の並列処理:不可
復号化の並列処理:可
ランダムリード :可 |
OFB
(Output Feedback) |
前ブロックの初期化ベクトルを暗号化した鍵ストリームを作成し、
鍵ストリームと平文とのXORをとる。
暗号化の並列処理:不可
復号化の並列処理:不可
ランダムリード :不可 |
CTR
(Counter) |
前ブロックのカウンタ値を暗号化した鍵ストリームを作成し、
鍵ストリームと平文とのXORをとる。一般的に利用される。
暗号化の並列処理:可
復号化の並列処理:可
ランダムリード :可 |
GCM
(Galois/Counter Mode) |
CTRモードに認証機能(Galois mode)を組み合わせたモード。
近年、利用が増えているらしい
暗号化の並列処理:可
復号化の並列処理:可
ランダムリード :可 |
パディング方式
ブロック暗号で暗号利用モードを用いて長文を暗号化する場合、長文の文字数は『ブロック長×ブロック数』でなければいけないという制約がある。この制約を緩和するために用いるのがパディングで、長文の最終ブロックの文字数をブロック長に等しくなるように無意味な文字で埋める処理のことを指す。パディング方式は埋める文字を指定する方法であり、null値やパディング文字数を表す数字などを指定することができる。よく利用されるパディング方式を以下に示す。
パディング方式 |
内容 |
NoPadding |
パディングなし |
ZeroBytePadding |
null値でパディング |
PKCS5Padding |
パディング文字数を表す数字でパディング |
■ Java 8における暗号の利用
Javaの言語仕様上、利用可能であることが保証されている暗号方式の組み合わせ(
*8)を以下に示す。特に理由がない場合を除き、AESかRSAを利用することになると思われる。下記に加えて、環境によっては利用可能な暗号方式の組み合わせが存在する。詳細はOracleのwebサイトを参照のこと(
*2)。
アルゴリズム |
利用モード |
パディング |
DES(56bit) |
ECB |
NoPadding |
PKCS5Padding |
CBC |
NoPadding |
PKCS5Padding |
DESede(168bit) |
ECB |
NoPadding |
PKCS5Padding |
CBC |
NoPadding |
PKCS5Padding |
AES(128bit) |
ECB |
NoPadding |
PKCS5Padding |
CBC |
NoPadding |
PKCS5Padding |
RSA (1024bit / 2048bit) |
ECB |
PKCS1Padding |
OAEPWithSHA-1AndMGF1Padding |
OAEPWithSHA-256AndMGF1Padding |
Javaプログラムで暗号を利用するには以下の手順となる。
- Cipher::getInstance関数を呼び出し、暗号インスタンス(Cipherクラス)を取得する
- Cipher::init関数で暗号インスタンスを初期化する
- Cipher::update関数を用いて暗号化を実施。(最終ブロック以外)
- Cipher::doFinal関数を用いて暗号化を実施。(最終ブロックのみ)
手順1については『Cipher.getInstance( "AES/CBC/PKCS5Padding" )』のように、Cipher::getInstance関数に対して暗号方式の組み合わせを『/(スラッシュ)』で区切った文字列で指定する。手順2ではinit関数の引数として、暗号化の場合には「Cipher.ENCRYPT_MODE」を、復号化する場合には「Cipher.DECRYPT_MODE」を指定する。詳細はサンプルプログラムを参照のこと。
プログラミング上の注意点としては、パスワード文字列を利用する場合はStringではなくByte[]で保持すべき点があげられる。リファレンスで以下のように記述されているとおり、必要な時以外はパスワード情報をメモリ上から消去するためである。
パスワードベース暗号化の使用
~略~
パスワードを収集し、java.lang.String型のオブジェクトに格納するのは、適切と考えられます。ただし、注意すべき点があります。それは、String型のオブジェクトは不変であるということです。このため、使用後にStringの内容を変更(上書き)またはゼロにするようなメソッドは存在しません。この機能のために、Stringオブジェクトは、ユーザー・パスワードなどセキュリティ上重要な情報の格納には適しません。セキュリティ関連の情報は、常にchar型の配列に収集および格納するようにしてください。
■ サンプル・プログラム(文字列の暗号化・復号化)
以下に文字列を暗号化・復号化するサンプル・プログラムを示す。サンプルプログラムではアルゴリズム=AES(128bit)、暗号モード=CBC、パディング方式PKCS5Paddingで『This is plain text.』という文字列を暗号化、復号化している。
◇サンプルコード
package application;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
/**
* AES暗号化のテスト
* @author karura
*
*/
public class TestCipherAES128
{
// 定数
private static final int KEY_LENGTH = 128; // 共通鍵の長さ[bit]
private static final int BLOCK_LENGTH = 128; // ブロック長[bit]
/**
* 起動関数
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception
{
new TestCipherAES128();
}
/**
* コンストラクタ
* @throws Exception
*/
public TestCipherAES128() throws Exception
{
// AES暗号オブジェクト・CBC用の初期ベクトルの作成
Cipher c = Cipher.getInstance( "AES/CBC/PKCS5Padding" );
IvParameterSpec iv = new IvParameterSpec( new byte[ BLOCK_LENGTH / 8 ] );
// AES暗号鍵(SecureRandomで作成したランダム値)の作成
Key key = null;
KeyGenerator keyGen = KeyGenerator.getInstance( "AES" );
keyGen.init( KEY_LENGTH );
key = keyGen.generateKey();
// AES暗号鍵(指定値)の作成。任意の文字列(128bit)を利用する場合
//byte[] keyStr = "1234567890123456".getBytes();
//Key key = new SecretKeySpec( keyStr , "AES");
// 共通鍵
System.out.println( String.format( "Key(hex) : %s" , toHexStr( key.getEncoded() ) ) );
// 平文
byte[] plain = "This is plain text.".getBytes();
System.out.println( String.format( "Plain : %s" , new String( plain ) ) );
System.out.println( String.format( "Plain(hex) : %s" , toHexStr( plain ) ) );
// 暗号文
c.init( Cipher.ENCRYPT_MODE , key , iv );
byte[] encode = c.doFinal( plain );
System.out.println( String.format( "Encode : %s" , new String( encode ) ) );
System.out.println( String.format( "Encode(hex) : %s" , toHexStr( encode ) ) );
// 復号文
c.init( Cipher.DECRYPT_MODE, key , iv );
byte[] decode = c.doFinal( encode );
System.out.println( String.format( "Decode : %s" , new String( decode ) ) );
System.out.println( String.format( "Decode(hex) : %s" , toHexStr( decode ) ) );
}
/**
* byte配列を16進数表記の文字列に変換
* @param bytes
* @return
*/
protected String toHexStr( byte[] bytes )
{
// 戻り値用変数の準備
String hexStr = "";
// byte配列の要素を1つずつ16進数に変換
for( byte b : bytes ){ hexStr += String.format( "%02x" , b ); }
// 戻り値を返す
return hexStr;
}
}
◇実行結果
Key(hex) : 950b1a3c115f19304edf5673392b54f2
Plain : This is plain text.
Plain(hex) : 5468697320697320706c61696e20746578742e
Encode : e�O��'��6��a���:fp��]ǹ�d1��
Encode(hex) : 65fc4fdbd62715aa1bac3683f76186bdfa3a6670d3fd115dc7b9ad64318c9516
Decode : This is plain text.
Decode(hex) : 5468697320697320706c61696e20746578742e
◇解説
プログラムの流れとしては、暗号化インスタンスcを作成(37行目)後、暗号化に必要な初期化ベクトルivと共通鍵keyを作成して暗号化・復号化を行っている。その途中、確認のために共通鍵・平文・暗号文・復号文を標準出力している。
実行結果の鍵の内容を見てみると32桁の16進数であるため、鍵長さは『32×4[bit]=128bit』であることが確認できる。また、今回は鍵として乱数を利用したが、パスワードを用いる場合は47行目~48行目のコメント部のような記述をすればよい。
暗号モードがCBCであるため、暗号化・復号化時に初期化ベクトルivを利用している。このため、通信路の暗号化で利用する際には、どうにかして鍵keyと初期化ベクトルivを通信元と通信先で事前共有する必要がある。もちろん、ECBを利用する際には初期化ベクトルは必要ない。
また、今回のサンプルでは平文が短いためCipher::update関数は利用せず、Cipher::doFinal関数のみ利用している点に注意が必要である。
■ サンプル・プログラム(ファイルの暗号化・復号化)
以下にファイルを暗号化・復号化するサンプル・プログラムを示す。サンプルプログラムではアルゴリズム=RSA(1024bit)、暗号モード=ECB、パディング方式PKCS1Paddingで『This is plain text.』という文字列を暗号化、復号化している。
◇リソース
TestJava(プロジェクトフォルダ)
┣ src
┃ ┗ application
┃ ┗ TestCipherFile.java
┣ input
┃ ┗ plain.txt
┗ output
◇サンプルコード
package application;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
/**
* AES暗号化のテスト
* @author karura
*
*/
public class TestCipherFile
{
// 定数
private static final int KEY_LENGTH = 1024; // 鍵の長さ[bit]
private static final int BLOCK_LENGTH = 128; // ブロック長[bit]
/**
* 起動関数
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception
{
new TestCipherFile();
}
/**
* コンストラクタ
* @throws Exception
*/
public TestCipherFile() throws Exception
{
// 暗号化オブジェクト・初期ベクトルを作成
Cipher c = Cipher.getInstance( "RSA/ECB/PKCS1Padding" );
// RSA鍵作成クラスを初期化
KeyPairGenerator keyGen = KeyPairGenerator.getInstance( "RSA" );
keyGen.initialize( KEY_LENGTH );
// RSA鍵ペアを作成
KeyPair keyPair = keyGen.generateKeyPair();
Key privateKey = keyPair.getPrivate();
Key publicKey = keyPair.getPublic();
// 鍵
System.out.println( String.format( "Private Key(hex) : %s" , toHexStr( privateKey.getEncoded() ) ) );
System.out.println( String.format( "Public Key(hex) : %s" , toHexStr( publicKey.getEncoded() ) ) );
// ファイルの暗号化
// try-with-resource構文を利用
c.init( Cipher.ENCRYPT_MODE , privateKey );
try( FileInputStream fis = new FileInputStream( new File( "input/plain.txt" ) );
FileOutputStream fos = new FileOutputStream( new File( "output/encode.txt" ) );
CipherOutputStream cos = new CipherOutputStream( fos , c ) )
{
// バッファ定義
int size = 0;
byte[] buf = new byte[ 1024 ];
// 暗号化してファイル出力
while( ( size = fis.read( buf ) ) > 0 )
{
// サイズを指定して出力
cos.write( buf , 0 , size );
}
cos.flush();
}
// ファイルの復号化
// try-with-resource構文を利用
c.init( Cipher.DECRYPT_MODE , publicKey );
try( FileInputStream fis = new FileInputStream( new File( "output/encode.txt" ) );
CipherInputStream cis = new CipherInputStream( fis , c );
FileOutputStream fos = new FileOutputStream( new File( "output/decode.txt" ) )
)
{
// バッファ定義
int size = 0;
byte[] buf = new byte[ 1024 ];
// 復号化してファイル出力
while( ( size = cis.read( buf ) ) > 0 )
{
// サイズを指定して出力
fos.write( buf , 0 , size );
}
fos.flush();
}
// 処理終了メッセージ
System.out.println( "end." );
}
/**
* byte配列を16進数表記の文字列に変換
* @param bytes
* @return
*/
protected String toHexStr( byte[] bytes )
{
// 戻り値用変数の準備
String hexStr = "";
// byte配列の要素を1つずつ16進数に変換
for( byte b : bytes ){ hexStr += String.format( "%02x" , b ); }
// 戻り値を返す
return hexStr;
}
}
◇実行結果
TestJava(プロジェクトフォルダ)
┗ output
┣ encode.txt
┗ decode.txt
Private Key(hex) : 30820278020100300d06092a864886f70d01010…
Public Key(hex) : 30819f300d06092a864886f70d0101010500038…
end.
◇解説
ファイルの暗号化・復号化は難しくなく、ファイルの入出力処理にCipherInputStream・CipherOutputStreamをかませればいいだけである。その際に、暗号インスタンスを引数に渡すことになるが、これも文字列の暗号化で確認したようにCipher::getIntance関数で取得後、Cipher::init関数で初期化すればよい(42行目~59行目)。今回はRSA暗号を利用するため、鍵の作成にはKeyPairGeneratorクラスを利用している点に注目である。
プログラムの記述方法について確認すると、60行目~75行目の『try( ○○ ){ ×× }』という記述が見慣れないと思われるが、try-with-resource構文(
*10)といい、○○部で宣言した変数については、tryの処理部(波括弧)が終了した際に自動的にcloseされるという構文である。これはファイルのopen/close処理を行う場合にexception記述が煩雑になるのを避けるための構文であり、ソースの可読性を上げるための構文である。ただし、○○部でなんでも宣言できるというわけでなく java.lang.AutoCloseableインタフェースを実装したクラスのみ宣言可能となっている。このインターフェイスはInputStreamやOutputStreamを継承していれば、たいていのクラスで利用可能である。
■ 参照
- Oracle 「Java暗号化アーキテクチャ(JCA)リファレンス・ガイド」
- Oracle 「Java暗号化アーキテクチャOracleプロバイダのドキュメント(JDK 8用)」
- RSA LABORATORIES 「DES CHALLENGE III」
- 情報処理推進機構(IPA)「SSL/TLS 暗号設定 ガイドライン」
- Wikipedia 「暗号利用モード」
- 情報通信研究機構 「2008 年度版リストガイド (秘匿の暗号利用モード) 」
- Wikipedia 「Galois/Counter Mode」
- JavaDoc 「クラスCipher」
- Oracle 「Java SE Documentation : try-with-resources 文」
- Wikipedia 「Data Encryption Standard」
- Wikipedia 「Advanced Encryption Standard」
- Wikipedia 「RC2」
- Wikipedia 「RC5」
- Wikipedia 「楕円曲線暗号」
- Pentan.info 「[暗号化]ブロック暗号とは(AES/DES/Blowfish PKCS5Padding ECB/CBC IV)」
1. 長文ファイルを暗号化する方法について